查看原文
其他

KVC原理与数据筛选

宋宏帅 京东技术 2024-03-22


Tech

导读

      通过分析Foundation框架中的KVC部分并结合案例分析KVC原理,解释为什么属性为简单数据类型的时候可以设置其值为字符串类型且不会崩溃的真实原因。最后举例说明KVC使用的场景和高级应用。




01 前言

在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!

      在技术论坛中看到一则很有意思的KVC案例:

【Objective-c】@interface Person : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSInteger age;@endPerson *person = [Person new];person.name = @"Tom";person.age = 10;[person setValue:@"100" forKey:@"age"];//此处赋值为字符串,类中属性为Integer
      第一反应是崩溃,因为OC是类型敏感的。可是在实现并打印后的结果出于意料,没有崩溃且赋值成功。所以有了深入了解KVC的内部实现的想法!


02   什么是KVC  

理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。从设计稿出发,提升页面搭建效率,亟需解决的核心问题有:
key-value-coding:键值编码,一种可以通过键名间接访问和赋值对象属性的机制;KVC是通过NSObject、NSArray、NSDictionary等的类别来实现的。主要方法包括一下几个:
【Objective-c】- (nullable id)valueForKey:(NSString *)key;- (void)setValue:(nullable id)value forKey:(NSString *)key;- (void)setNilValueForKey:(NSString *)key;- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;- (nullable id)valueForUndefinedKey:(NSString *)key;


03   KVC执行分析

理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。从设计稿出发,提升页面搭建效率,亟需解决的核心问题有:

      那么上面的案例中的- (void)setValue:(nullable id)value forKey:(NSString *)key;是怎样的执行过程呢?借助反汇编工具获得Foundation.framework部分源码(为了解决和系统API冲突问题增加前缀_d,NS替换为DS),以此分析KVC执行过程。(流程中的边界判断等已经忽略,如想了解可以参考源码,本文只探究主流程。)

3.1 设置属性




3.1.1 查找访问器方法或成员变量

【Objective-c】+ (DSKeyValueSetter *)_d_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueSetter *setter = nil; char key_cstr_upfirst[key_cstr_len + 1]; key_cstr[key_cstr_len + 1]; ... Method method = NULL; //按顺序寻找set<Key>,_set<Key>,setIs<Key>。找到后则生成对应的seter if ((method = DSKeyValueMethodForPattern(self, "set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "_set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "setIs%s:", key_cstr_upfirst)) ) { //生成Method:包含selector,IMP。返回和参数类型字符串 setter = [[DSKeyValueMethodSetter alloc] initWithContainerClassID:containerClassID key:key method:method]; } else if ([self accessInstanceVariablesDirectly]) {//如果没有找到对应的访问器方且工厂方法accessInstanceVariablesDirectly == ture ,则按照顺序查找查找成员变量_<key>,_is<Key>,<key>,is<Key>(注意key的首字母大小写,查找到则生成对应的setter) Ivar ivar = NULL; if ((ivar = DSKeyValueIvarForPattern(self, "_%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", key_cstr_upfirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", key_cstr_upfirst)) ) { setter = [[DSKeyValueIvarSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } ... return setter;}
查找顺序如下:
1. 查找访问器方法:set<key>,_set<key>,setIs<key>;
2. 如果步骤1中没找到对应的方法且 accessInstanceVariablesDirectly == YES,
则查找顺序如下:_<key>,_is<key>,<key>,is<key>;
查找不到则调用valueForUndefinedKey并抛出异常。
3.1.2 生成setter
【Objective-c】+ (DSKeyValueSetter *)_d_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { return [[DSKeyValueUndefinedSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self];} //构造方法确定方法编号 d_setValue:forUndefinedKey: 和方法指针IMP _DSSetValueAndNotifyForUndefinedKey- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa { ... return [super initWithContainerClassID:containerClassID key:key implementation:method_getImplementation(class_getInstanceMethod(containerIsa, @selector(d_setValue:forUndefinedKey:))) selector:@selector(d_setValue:forUndefinedKey:) extraArguments:arguments count:1];}

图1 生成结果,调用的对应IMP为int类型

3.1.3 赋值
      基本的访问器方法、变量的查找和异常处理已经清楚的知道了。那么上面的例子是如何出现的呢?明明传入的是字符串,最后赋值的时候转变为访问器方法所对应的类型?让我们继续刨根问底!
      DSKeyValueSetter对象已经生成,即确定了发送消息的对象object、访问器方法名SEL、访问器函数指针IMP、以及使用KVC时传入的Key和Value。下面进入方法调用阶段:_DSSetUsingKeyValueSetter(self,setter, value);

图2 拆包结果:IMP指针为_DSSetIntValueForKeyWithMethod。
      其定义如下:之所以有文章开头提到的效果就是这里起了作用,在IMP调用的时候做了[value valueGetSelectorName],将对应的NSNumber转换为简单数据类型。这里是intValue。
【Objective-c】void _DSSetIntValueForKeyWithMethod(id object, SEL selector,id value, NSString *key, Method method) {// object:person selector:setAge: value:@(100) key:age method:selector + IMP + 返回类型和参数类型 即_extraArgument2,其在第一步查找到访问器方法后生成 __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, int, intValue);}#define __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, valueType, valueGetSelectorName) do {\ if (value) {\ void (*imp)(id,SEL,valueType) = (void (*)(id,SEL,valueType))method_getImplementation(method);\ imp(object, method_getName(method), [value valueGetSelectorName]);\调用person的setAge:方法。参数为100 }\ else {\ [object setNilValueForKey:key];\ }\}while(0)//如果第一步中没有找到访问器方法只找到了成员变量则直接执行赋值操作void _DSSetIntValueForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar) { if (value) { *(int *)object_getIvarAddress(object, ivar) = [value intValue]; } else { [object setNilValueForKey:key]; }}

起始问题得到完美解决。

图3 KVC执行流程图

    3.2 取值



    3.2.1 查找访问器方法或成员变量

    【Objective-c】+ (DSKeyValueGetter *)_d_createValueGetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueGetter * getter = nil; ... Method getMethod = NULL; if((getMethod = DSKeyValueMethodForPattern(self,"get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"%s",keyCStr)) || (getMethod = DSKeyValueMethodForPattern(self,"is%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_%s",keyCStr))) { getter = [[DSKeyValueMethodGetter alloc] initWithContainerClassID:containerClassID key:key method:getMethod]; }// 查找对应的访问器方法 ... else if([self accessInstanceVariablesDirectly]) {//查找属性 Ivar ivar = NULL; if((ivar = DSKeyValueIvarForPattern(self, "_%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", keyCStrUpFirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", keyCStrUpFirst)) ) { getter = [[DSKeyValueIvarGetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } } if(!getter) { getter = [self _d_createValuePrimitiveGetterWithContainerClassID:containerClassID key:key]; } return getter;}

    1. 按照get<Key>,<key>,is<Key>,_<key>的顺序查找成员方法

    2. 如果1中没有找到对应的方法且accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_<key>,_is<Key>,<key>,is<Key>

    3. 如果1,2没有找到对应的方法和属性则调用 valueForUndefinedKey:并抛出异常

    4. 技术类:明确是否为需求/技术层面引起的风险;

    3.2.2 如上步骤没定位到访问器方法或成员变量则走下面的流程生成对应的getter

    【Objective-c】访问器方法生成IMP- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key method:(Method)method { NSUInteger methodArgumentsCount = method_getNumberOfArguments(method); NSUInteger extraAtgumentCount = 1; if(methodArgumentsCount == 2) { char *returnType = method_copyReturnType(method); IMP imp = NULL; switch (returnType[0]) { ... case 'i': { imp = (IMP)_DSGetIntValueWithMethod; } break; ... free(returnType); if(imp) { void *arguments[3] = {0}; if(extraAtgumentCount > 0) { arguments[0] = method; } return [super initWithContainerClassID:containerClassID key:key implementation:imp selector:method_getName(method) extraArguments:arguments count:extraAtgumentCount]; }}

    单步调试后可以看到具体的IMP类型:

    图4 get方法IMP类型
    定义如下:
    【Objective-c】NSNumber * _DSGetIntValueWithMethod(id object, SEL selctor, Method method) {// return [[[NSNumber alloc] initWithInt: ((int (*)(id,SEL))method_getImplementation(method))(object, method_getName(method))] autorelease];}
    3.2.3 取值
    取值调用如下:
    图5 get IMP调用


    04   简单数据类型KVC包装和拆装关系   

    理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。

    NSNunber:

    数据类型

    包装方法

    拆装方法

    BOOL

    numberWithBool:

    boolValue

    char

    numberWithChar:

    charValue

    double

    numberWithDouble:

    doubleValue

    float

    numberWithFloat:

    floatValue

    int

    numberWithInt:

    intValue

    long

    numberWithLong

    longValue

    long long

    numberWithLongLong:

    longLongValue

    short

    numberWithShort:

    shortValue

    unsigned char

    numberWithUnsignedChar:

    unsignedChar

    unsigned int

    numberWithUnsignedInt:

    unsignedInt

    unsigned long

    numberWithUnsignedLong:

    unsignedLong

    unsigned long long

    numberWithUnsignedLongLong:

    unsignedLongLong

    unsigned short

    numberWithUnsignedShort:

    unsignedShort

    表1  NSNunber

    NSValue:

    数据类型

    包装方法

    拆装方法

    NSPoint

    valueWithPoint:

    pointValue

    NSRange

    valueWithRange:

    rangeValue

    NSRect

    valueWithRect: (macOS)

    rectValue

    NSSize

    valueWithSize:

    sizeValue

    表2  NSValue



    05   KVC高级  

    理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。从设计稿出发,提升页面搭建效率,亟需解决的核心问题有:
    修改数组中对象的属性[array valueForKeyPath:@"uppercaseString"]利用KVC可以批量修改属性的成员变量值求和,平均数,最大值,最小值NSNumber*sum= [array valueForKeyPath:@"@sum.self"];NSNumber*avg= [array valueForKeyPath:@"@avg.self"];NSNumber*max= [array valueForKeyPath:@"@max.self"];NSNumber*min= [array valueForKeyPath:@"@min.self"];


    06   数据筛选  

    理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。从设计稿出发,提升页面搭建效率,亟需解决的核心问题有:
          经过上面的分析可以明白KVC的真正执行流程。下面结合日常工程中的实际应用来优雅的处理数据筛选问题。使用KVC处理可以减少大量for的使用并增加代码可读性和健壮性。

    图6 应用场景      项目中的细节如下:修改拒收数量时更新总妥投数和总拒收数、勾选明细更新总妥投数和总拒收数、全选、清空、反选。如果用通常的做法是每次操作都要循环去计算总数和记录选择状态。下面是采用KVC的实现过程。模型涉及:
    【Objective-c】@property (nonatomic,copy)NSString* skuCode;@property (nonatomic,copy)NSString* goodsName;@property (nonatomic,assign)NSInteger totalAmount;@property (nonatomic,assign)NSInteger rejectAmount;@property (nonatomic,assign)NSInteger deliveryAmount;///单选用@property (nonatomic, assign) BOOL selected;1. 更新总数
    【Objective-c】- (void)updateDeliveryInfo { //总数 NSNumber *allDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.totalAmount"]; //妥投数 NSNumber *allRealDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.deliveryAmount"]; //拒收数 NSNumber *allRejectAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.rejectAmount"];}2. 全选
    【Objective-c】[self.orderDetailModel.deliveryGoodsDetailList setValue:@(YES) forKeyPath:@"selected"];3. 清空
    【Objective-c】[self.orderDetailModel.deliveryGoodsDetailList setValue:@(NO) forKeyPath:@"selected"];4. 反选
    【Objective-c】NSPredicate *selectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(YES)];NSArray *selectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:selectedPredicate];NSPredicate *unSelectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(NO)];NSArray *unSelectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:unSelectedPredicate];[selectedArray setValue:@(NO) forKeyPath:@"selected"];[unSelectedArray setValue:@(YES) forKeyPath:@"selected"];1. 更新 gengxin总数
    07   总结  

    理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。从设计稿出发,提升页面搭建效率,亟需解决的核心问题有:

          KVC在处理简单数据类型时会经过数据封装和拆装并转换为对应的数据类型。通过KVC的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。



    推荐阅读测试角色在项目各阶段的项目管理tips
    会员权益核心引擎ZCube原理与实践数据驱动测试-从方法探研到最佳实践
    国际计费系统基于Sharding-Proxy大数据迁移方案实践

    求分享

    求点赞

    求在看

    继续滑动看下一个
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存